/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as sinon from 'sinon';
import type * as nbformat from '@jupyterlab/nbformat';
import * as assert from 'assert';
import * as vscode from 'vscode';
import { jupyterCellOutputToCellOutput, jupyterNotebookModelToNotebookData } from '../deserializers';
import { createMarkdownCellFromNotebookCell, getCellMetadata } from '../serializers';

function deepStripProperties(obj: any, props: string[]) {
	for (const prop in obj) {
		if (obj[prop]) {
			delete obj[prop];
		} else if (typeof obj[prop] === 'object') {
			deepStripProperties(obj[prop], props);
		}
	}
}
[true, false].forEach(useCustomPropertyInMetadata => {
	suite(`ipynb serializer (${useCustomPropertyInMetadata ? 'with custom metadata (standard behaviour)' : 'without custom metadata'})`, () => {
		let disposables: vscode.Disposable[] = [];
		setup(() => {
			disposables = [];
			sinon.stub(vscode.workspace, 'getConfiguration').callsFake((section, scope) => {
				if (section === 'jupyter') {
					return {
						get: () => {
							return !useCustomPropertyInMetadata;
						}
					};
				} else {
					return (vscode.workspace.getConfiguration as any).wrappedMethod.call(vscode.workspace, section, scope);
				}
			});
		});
		teardown(async () => {
			disposables.forEach(d => d.dispose());
			disposables = [];
			sinon.restore();
		});

		const base64EncodedImage =
			'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOUlZL6DwAB/wFSU1jVmgAAAABJRU5ErkJggg==';
		test('Deserialize', async () => {
			const cells: nbformat.ICell[] = [
				{
					cell_type: 'code',
					execution_count: 10,
					outputs: [],
					source: 'print(1)',
					metadata: {}
				},
				{
					cell_type: 'markdown',
					source: '# HEAD',
					metadata: {}
				}
			];
			const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python');
			assert.ok(notebook);

			const expectedCodeCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, 'print(1)', 'python');
			expectedCodeCell.outputs = [];
			expectedCodeCell.metadata = useCustomPropertyInMetadata ? { custom: { execution_count: 10, metadata: {} } } : { execution_count: 10, metadata: {} };
			expectedCodeCell.executionSummary = { executionOrder: 10 };

			const expectedMarkdownCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# HEAD', 'markdown');
			expectedMarkdownCell.outputs = [];
			expectedMarkdownCell.metadata = useCustomPropertyInMetadata ? {
				custom: { metadata: {} }
			} : {
				metadata: {}
			};

			assert.deepStrictEqual(notebook.cells, [expectedCodeCell, expectedMarkdownCell]);
		});


		test('Serialize', async () => {
			const markdownCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# header1', 'markdown');
			markdownCell.metadata = useCustomPropertyInMetadata ? {
				attachments: {
					'image.png': {
						'image/png': 'abc'
					}
				},
				custom: {
					id: '123',
					metadata: {
						foo: 'bar'
					}
				}
			} : {
				attachments: {
					'image.png': {
						'image/png': 'abc'
					}
				},
				id: '123',
				metadata: {
					foo: 'bar'
				}
			};

			const cellMetadata = getCellMetadata({ cell: markdownCell });
			assert.deepStrictEqual(cellMetadata, {
				id: '123',
				metadata: {
					foo: 'bar',
				},
				attachments: {
					'image.png': {
						'image/png': 'abc'
					}
				}
			});

			const markdownCell2 = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# header1', 'markdown');
			markdownCell2.metadata = useCustomPropertyInMetadata ? {
				custom: {
					id: '123',
					metadata: {
						foo: 'bar'
					},
					attachments: {
						'image.png': {
							'image/png': 'abc'
						}
					}
				}
			} : {
				id: '123',
				metadata: {
					foo: 'bar'
				},
				attachments: {
					'image.png': {
						'image/png': 'abc'
					}
				}
			};

			const nbMarkdownCell = createMarkdownCellFromNotebookCell(markdownCell);
			const nbMarkdownCell2 = createMarkdownCellFromNotebookCell(markdownCell2);
			assert.deepStrictEqual(nbMarkdownCell, nbMarkdownCell2);

			assert.deepStrictEqual(nbMarkdownCell, {
				cell_type: 'markdown',
				source: ['# header1'],
				metadata: {
					foo: 'bar',
				},
				attachments: {
					'image.png': {
						'image/png': 'abc'
					}
				},
				id: '123'
			});
		});

		suite('Outputs', () => {
			function validateCellOutputTranslation(
				outputs: nbformat.IOutput[],
				expectedOutputs: vscode.NotebookCellOutput[],
				propertiesToExcludeFromComparison: string[] = []
			) {
				const cells: nbformat.ICell[] = [
					{
						cell_type: 'code',
						execution_count: 10,
						outputs,
						source: 'print(1)',
						metadata: {}
					}
				];
				const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python');

				// OutputItems contain an `id` property generated by VSC.
				// Exclude that property when comparing.
				const propertiesToExclude = propertiesToExcludeFromComparison.concat(['id']);
				const actualOuts = notebook.cells[0].outputs;
				deepStripProperties(actualOuts, propertiesToExclude);
				deepStripProperties(expectedOutputs, propertiesToExclude);
				assert.deepStrictEqual(actualOuts, expectedOutputs);
			}

			test('Empty output', () => {
				validateCellOutputTranslation([], []);
			});

			test('Stream output', () => {
				validateCellOutputTranslation(
					[
						{
							output_type: 'stream',
							name: 'stderr',
							text: 'Error'
						},
						{
							output_type: 'stream',
							name: 'stdout',
							text: 'NoError'
						}
					],
					[
						new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('Error')], {
							outputType: 'stream'
						}),
						new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('NoError')], {
							outputType: 'stream'
						})
					]
				);
			});
			test('Stream output and line endings', () => {
				validateCellOutputTranslation(
					[
						{
							output_type: 'stream',
							name: 'stdout',
							text: [
								'Line1\n',
								'\n',
								'Line3\n',
								'Line4'
							]
						}
					],
					[
						new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Line1\n\nLine3\nLine4')], {
							outputType: 'stream'
						})
					]
				);
				validateCellOutputTranslation(
					[
						{
							output_type: 'stream',
							name: 'stdout',
							text: [
								'Hello\n',
								'Hello\n',
								'Hello\n',
								'Hello\n',
								'Hello\n',
								'Hello\n'
							]
						}
					],
					[
						new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Hello\nHello\nHello\nHello\nHello\nHello\n')], {
							outputType: 'stream'
						})
					]
				);
			});
			test('Multi-line Stream output', () => {
				validateCellOutputTranslation(
					[
						{
							name: 'stdout',
							output_type: 'stream',
							text: [
								'Epoch 1/5\n',
								'...\n',
								'Epoch 2/5\n',
								'...\n',
								'Epoch 3/5\n',
								'...\n',
								'Epoch 4/5\n',
								'...\n',
								'Epoch 5/5\n',
								'...\n'
							]
						}
					],
					[
						new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout(['Epoch 1/5\n',
							'...\n',
							'Epoch 2/5\n',
							'...\n',
							'Epoch 3/5\n',
							'...\n',
							'Epoch 4/5\n',
							'...\n',
							'Epoch 5/5\n',
							'...\n'].join(''))], {
							outputType: 'stream'
						})
					]
				);
			});

			test('Multi-line Stream output (last empty line should not be saved in ipynb)', () => {
				validateCellOutputTranslation(
					[
						{
							name: 'stderr',
							output_type: 'stream',
							text: [
								'Epoch 1/5\n',
								'...\n',
								'Epoch 2/5\n',
								'...\n',
								'Epoch 3/5\n',
								'...\n',
								'Epoch 4/5\n',
								'...\n',
								'Epoch 5/5\n',
								'...\n'
							]
						}
					],
					[
						new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr(['Epoch 1/5\n',
							'...\n',
							'Epoch 2/5\n',
							'...\n',
							'Epoch 3/5\n',
							'...\n',
							'Epoch 4/5\n',
							'...\n',
							'Epoch 5/5\n',
							'...\n',
							// This last empty line should not be saved in ipynb.
							'\n'].join(''))], {
							outputType: 'stream'
						})
					]
				);
			});

			test('Streamed text with Ansi characters', async () => {
				validateCellOutputTranslation(
					[
						{
							name: 'stderr',
							text: '\u001b[K\u001b[33m✅ \u001b[0m Loading\n',
							output_type: 'stream'
						}
					],
					[
						new vscode.NotebookCellOutput(
							[vscode.NotebookCellOutputItem.stderr('\u001b[K\u001b[33m✅ \u001b[0m Loading\n')],
							{
								outputType: 'stream'
							}
						)
					]
				);
			});

			test('Streamed text with angle bracket characters', async () => {
				validateCellOutputTranslation(
					[
						{
							name: 'stderr',
							text: '1 is < 2',
							output_type: 'stream'
						}
					],
					[
						new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('1 is < 2')], {
							outputType: 'stream'
						})
					]
				);
			});

			test('Streamed text with angle bracket characters and ansi chars', async () => {
				validateCellOutputTranslation(
					[
						{
							name: 'stderr',
							text: '1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n',
							output_type: 'stream'
						}
					],
					[
						new vscode.NotebookCellOutput(
							[vscode.NotebookCellOutputItem.stderr('1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n')],
							{
								outputType: 'stream'
							}
						)
					]
				);
			});

			test('Error', async () => {
				validateCellOutputTranslation(
					[
						{
							ename: 'Error Name',
							evalue: 'Error Value',
							traceback: ['stack1', 'stack2', 'stack3'],
							output_type: 'error'
						}
					],
					[
						new vscode.NotebookCellOutput(
							[
								vscode.NotebookCellOutputItem.error({
									name: 'Error Name',
									message: 'Error Value',
									stack: ['stack1', 'stack2', 'stack3'].join('\n')
								})
							],
							{
								outputType: 'error',
								originalError: {
									ename: 'Error Name',
									evalue: 'Error Value',
									traceback: ['stack1', 'stack2', 'stack3'],
									output_type: 'error'
								}
							}
						)
					]
				);
			});

			['display_data', 'execute_result'].forEach(output_type => {
				suite(`Rich output for output_type = ${output_type}`, () => {
					// Properties to exclude when comparing.
					let propertiesToExcludeFromComparison: string[] = [];
					setup(() => {
						if (output_type === 'display_data') {
							// With display_data the execution_count property will never exist in the output.
							// We can ignore that (as it will never exist).
							// But we leave it in the case of `output_type === 'execute_result'`
							propertiesToExcludeFromComparison = ['execution_count', 'executionCount'];
						}
					});

					test('Text mimeType output', async () => {
						validateCellOutputTranslation(
							[
								{
									data: {
										'text/plain': 'Hello World!'
									},
									output_type,
									metadata: {},
									execution_count: 1
								}
							],
							[
								new vscode.NotebookCellOutput(
									[new vscode.NotebookCellOutputItem(Buffer.from('Hello World!', 'utf8'), 'text/plain')],
									{
										outputType: output_type,
										metadata: {}, // display_data & execute_result always have metadata.
										executionCount: 1
									}
								)
							],
							propertiesToExcludeFromComparison
						);
					});

					test('png,jpeg images', async () => {
						validateCellOutputTranslation(
							[
								{
									execution_count: 1,
									data: {
										'image/png': base64EncodedImage,
										'image/jpeg': base64EncodedImage
									},
									metadata: {},
									output_type
								}
							],
							[
								new vscode.NotebookCellOutput(
									[
										new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png'),
										new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/jpeg')
									],
									{
										executionCount: 1,
										outputType: output_type,
										metadata: {} // display_data & execute_result always have metadata.
									}
								)
							],
							propertiesToExcludeFromComparison
						);
					});

					test('png image with a light background', async () => {
						validateCellOutputTranslation(
							[
								{
									execution_count: 1,
									data: {
										'image/png': base64EncodedImage
									},
									metadata: {
										needs_background: 'light'
									},
									output_type
								}
							],
							[
								new vscode.NotebookCellOutput(
									[new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')],
									{
										executionCount: 1,
										metadata: {
											needs_background: 'light'
										},
										outputType: output_type
									}
								)
							],
							propertiesToExcludeFromComparison
						);
					});

					test('png image with a dark background', async () => {
						validateCellOutputTranslation(
							[
								{
									execution_count: 1,
									data: {
										'image/png': base64EncodedImage
									},
									metadata: {
										needs_background: 'dark'
									},
									output_type
								}
							],
							[
								new vscode.NotebookCellOutput(
									[new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')],
									{
										executionCount: 1,
										metadata: {
											needs_background: 'dark'
										},
										outputType: output_type
									}
								)
							],
							propertiesToExcludeFromComparison
						);
					});

					test('png image with custom dimensions', async () => {
						validateCellOutputTranslation(
							[
								{
									execution_count: 1,
									data: {
										'image/png': base64EncodedImage
									},
									metadata: {
										'image/png': { height: '111px', width: '999px' }
									},
									output_type
								}
							],
							[
								new vscode.NotebookCellOutput(
									[new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')],
									{
										executionCount: 1,
										metadata: {
											'image/png': { height: '111px', width: '999px' }
										},
										outputType: output_type
									}
								)
							],
							propertiesToExcludeFromComparison
						);
					});

					test('png allowed to scroll', async () => {
						validateCellOutputTranslation(
							[
								{
									execution_count: 1,
									data: {
										'image/png': base64EncodedImage
									},
									metadata: {
										unconfined: true,
										'image/png': { width: '999px' }
									},
									output_type
								}
							],
							[
								new vscode.NotebookCellOutput(
									[new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')],
									{
										executionCount: 1,
										metadata: {
											unconfined: true,
											'image/png': { width: '999px' }
										},
										outputType: output_type
									}
								)
							],
							propertiesToExcludeFromComparison
						);
					});
				});
			});
		});

		suite('Output Order', () => {
			test('Verify order of outputs', async () => {
				const dataAndExpectedOrder: { output: nbformat.IDisplayData; expectedMimeTypesOrder: string[] }[] = [
					{
						output: {
							data: {
								'application/vnd.vegalite.v4+json': 'some json',
								'text/html': '<a>Hello</a>'
							},
							metadata: {},
							output_type: 'display_data'
						},
						expectedMimeTypesOrder: ['application/vnd.vegalite.v4+json', 'text/html']
					},
					{
						output: {
							data: {
								'application/vnd.vegalite.v4+json': 'some json',
								'application/javascript': 'some js',
								'text/plain': 'some text',
								'text/html': '<a>Hello</a>'
							},
							metadata: {},
							output_type: 'display_data'
						},
						expectedMimeTypesOrder: [
							'application/vnd.vegalite.v4+json',
							'text/html',
							'application/javascript',
							'text/plain'
						]
					},
					{
						output: {
							data: {
								'application/vnd.vegalite.v4+json': '', // Empty, should give preference to other mimetypes.
								'application/javascript': 'some js',
								'text/plain': 'some text',
								'text/html': '<a>Hello</a>'
							},
							metadata: {},
							output_type: 'display_data'
						},
						expectedMimeTypesOrder: [
							'text/html',
							'application/javascript',
							'text/plain',
							'application/vnd.vegalite.v4+json'
						]
					},
					{
						output: {
							data: {
								'text/plain': 'some text',
								'text/html': '<a>Hello</a>'
							},
							metadata: {},
							output_type: 'display_data'
						},
						expectedMimeTypesOrder: ['text/html', 'text/plain']
					},
					{
						output: {
							data: {
								'application/javascript': 'some js',
								'text/plain': 'some text'
							},
							metadata: {},
							output_type: 'display_data'
						},
						expectedMimeTypesOrder: ['application/javascript', 'text/plain']
					},
					{
						output: {
							data: {
								'image/svg+xml': 'some svg',
								'text/plain': 'some text'
							},
							metadata: {},
							output_type: 'display_data'
						},
						expectedMimeTypesOrder: ['image/svg+xml', 'text/plain']
					},
					{
						output: {
							data: {
								'text/latex': 'some latex',
								'text/plain': 'some text'
							},
							metadata: {},
							output_type: 'display_data'
						},
						expectedMimeTypesOrder: ['text/latex', 'text/plain']
					},
					{
						output: {
							data: {
								'application/vnd.jupyter.widget-view+json': 'some widget',
								'text/plain': 'some text'
							},
							metadata: {},
							output_type: 'display_data'
						},
						expectedMimeTypesOrder: ['application/vnd.jupyter.widget-view+json', 'text/plain']
					},
					{
						output: {
							data: {
								'text/plain': 'some text',
								'image/svg+xml': 'some svg',
								'image/png': 'some png'
							},
							metadata: {},
							output_type: 'display_data'
						},
						expectedMimeTypesOrder: ['image/png', 'image/svg+xml', 'text/plain']
					}
				];

				dataAndExpectedOrder.forEach(({ output, expectedMimeTypesOrder }) => {
					const sortedOutputs = jupyterCellOutputToCellOutput(output);
					const mimeTypes = sortedOutputs.items.map((item) => item.mime).join(',');
					assert.equal(mimeTypes, expectedMimeTypesOrder.join(','));
				});
			});
		});
	});
});
